API Gateway の Lambda プロキシ統合のCORS対応をまとめてみる
はじめに
おはようございます、もきゅりんです。
あけましておめでとうございます。
今回は、Lambda プロキシ統合のCORS対応をまとめてみました。
まず、この記事の内容に興味はあるのだが、Lambda プロキシ統合とかカスタム統合(非プロキシ統合)とか何?という方はこちらご参照下さい。
[初心者向け] Lambda 非プロキシ統合で API Gateway API をビルドする をプロキシ統合にして比較してみる
1 CORSについておさらい
CORSって何ぞや、または何となく知ってるけども、フワフワしておるという方は下記記事が非常におすすめです。
CORS(Cross-Origin Resource Sharing)について整理してみた
とにかく、API Gateway REST API(Lambda または HTTP プロキシ統合) の CORSの対応について知りたいという方は下表をご確認下さい。
クロスオリジンか | シンプルリクエストである | やること | 内容 |
---|---|---|---|
◯ | ◯ | レスポンス対応1 | Access-Control-Allow-Origin ヘッダー |
◯ | ☓ | CORS有効化 *1 + レスポンス対応2 | Access-Control-Allow-Origin , Access-Control-Allow-Headers ヘッダー |
☓ | ☓ | なし | - |
内容としては、以下 2つをチェックします。
- クロスオリジン HTTP リクエストかどうか
- シンプルなリクエストかシンプルではないリクエストかどうか
API Gateway REST API リソースの CORS を有効にする の再掲です。
1. クロスオリジン HTTP リクエストかどうか
以下クロスオリジンHTTPリクエストに当てはまるケースです。
- 別ドメイン (例: example.com から amazondomains.com へ)
- 別サブドメイン (例: example.com から petstore.example.com へ)
- 別ポート (例: example.com から example.com:10777 へ)
- 別プロトコル (例: https://example.com から http://example.com へ)
2. シンプルなリクエストかシンプルではないリクエストかどうか
以下条件がすべて当てはまる場合、シンプルなリクエストです。当てはまらない場合は、シンプルではないリクエストです。
- GET、HEAD、および POST のいずれかのメソッドリクエスト
- POST メソッドリクエストの場合、Origin ヘッダーを含まれている
- Content-Typeは text/plain、multipart/form-data、または application/x-www-form-urlencoded のどれか
- リクエストにカスタムヘッダーが含まれていない
- シンプルなリクエストに関する Mozilla CORS のドキュメント に一覧表示されている追加要件
整理できましたでしょうか。
Lambda または HTTP プロキシ統合以外の対応については、別途ドキュメント をご確認下さい。
実際に試してみる
まとめた内容をS3とAPI Gatewayを使って実際にやってみます。
構築は簡単な構成なので、Serverless Frameworkを使います。
Serverless Frameworkで何をやっているかを確認したい方はこちら参考にして下さい。
Serverless Framework / Quick Start
[初めてのサーバーレスアプリケーション開発 ~Serverless Framework を使ってAWSリソースをデプロイする~
環境は設定が楽なので、上記と同様 Cloud9 で進めていきます。
GETしてみる
適当なスペックのインスタンスを立ち上げて、Serverless Frameworkをインストールしたら、サービスを作成します。
$ serverless create --template aws-python3 --path demo-cors-api Serverless: Generating boilerplate... Serverless: Generating boilerplate in "/home/ec2-user/environment/demo-cors-api" _______ __ | _ .-----.----.--.--.-----.----| .-----.-----.-----. | |___| -__| _| | | -__| _| | -__|__ --|__ --| |____ |_____|__| \___/|_____|__| |__|_____|_____|_____| | | | The Serverless Application Framework | | serverless.com, v1.61.3 -------' Serverless: Successfully generated boilerplate for template: "aws-python3"
handler.py
は、そのままでも問題ないのですが、いちおう書き換えておきます。
import json def lambda_handler(event, context): return { 'statusCode': 200, 'body': json.dumps({"result": "GET Method Success."}) }
serverless.yml
を下記のように書き換えます。
リージョンをap-northeast-1
に設定します。
API Gatewayのエンドポイントは、デフォルトでエッジ最適化で作成してしまうようなので、リージョンに設定します。
service: demo-cors-api provider: name: aws runtime: python3.8 endpointType: REGIONAL region: ap-northeast-1 functions: lambda_handler: handler: handler.lambda_handler events: - http: path: / method: GET
そしたらデプロイします。
$ cd demo-cors $ sls deploy -v
出来上がったらエンドポイントのURLが表示されるので控えておきます。
... Stack Outputs LambdaUnderscorehandlerLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:xxxxxxx:function:demo-cors-dev-lambda_handler:1 ServiceEndpoint: https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev ServerlessDeploymentBucketName: demo-cors-dev-serverlessdeploymentbucket-xxxxxxxx
次に、今回はServerlessFramework経由で静的ファイルをS3にアップロードしたいので、プラグインをインストールします。
$ npm install --save serverless-s3-sync
static
というディレクトリを作成して、その中にindex.html
を作成します。
$ mkdir static && touch static/index.html
index.html
を更新します。
var URL
には控えたエンドポイントのURLを代入します。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <title>DemoForm</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <script> $(function() { var URL = 'APIGATEWAY_ENDPOINT'; $('#submit').click(function() { $.ajax({ method: 'GET', url: URL }) .done(function(msg) { console.log(msg); alert('success'); }) .fail(function(msg) { console.log(msg); alert('error'); }) .always(function() { alert('complete'); }); }); }); </script> </head> <body> <input type="button" id="submit" value="Go" /> </body> </html>
serverless.yml
に下記追記します。
... ... custom: webSiteName: s3-demo-site s3Sync: - bucketName: ${self:custom.webSiteName} localDir: static resources: Resources: StaticSite: Type: AWS::S3::Bucket Properties: BucketName: ${self:custom.webSiteName} AccessControl: PublicRead WebsiteConfiguration: IndexDocument: index.html StaticSiteS3BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: StaticSite PolicyDocument: Statement: - Sid: PublicReadGetObject Effect: Allow Principal: "*" Action: - s3:GetObject Resource: Fn::Join: ["", ["arn:aws:s3:::",{"Ref": "StaticSite"},"/*"]] plugins: - serverless-s3-sync
そしたら再度デプロイします。
$ sls deploy -v
出来たらS3のStatic website hosting
のエンドポイントを表示します。
GET
と書かれた素朴なページがありますので、クリックします。
エラー表示されました。
表を確認します。
そうでした、クロスドメインなので、レスポンス対応1をしないといけないのでした。
レスポンス対応1はAccess-Control-Allow-Origin
ヘッダーが必要です。
ということで、handler.py
を修正してデプロイします。
import json def lambda_handler(event, context): return { 'statusCode': 200, 'headers': { "Access-Control-Allow-Origin": "*" }, 'body': json.dumps({"result": "GET Method Success."}) }
完了したらまたクリックします。
OKOK〜成功フォ〜。
POSTしてみる
じゃ次はPOSTしてみましょう。
handler.py
とindex.html
、serverless.yml
をそれぞれ下記のように更新してデプロイします。
# handler.py import json def lambda_handler(event, context): print(event['httpMethod']) return { 'statusCode': 200, 'headers': { "Access-Control-Allow-Origin": "*" }, 'body': json.dumps(event['body']) }
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <title>DemoForm</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <script> $(function() { var URL = 'APIGATEWAY_ENDPOINT'; $('#submit').click(function() { var JSONdata = { name: $('#send-name').val(), subject: $('#send-subject').val(), checked: $('input:radio:checked').val() }; $.ajax({ method: 'POST', url: URL, dataType: 'json', contentType: "application/json", data: JSON.stringify(JSONdata) }) .done(function(msg) { console.log(msg); alert('success'); }) .fail(function(msg) { console.log(msg); alert('error'); }) .always(function() { alert('complete'); }); }); }); </script> </head> <body> <p> Name:<br /> <input type="text" id="send-name" name="name" /> </p> <p> Subject:<br /> <input type="text" id="send-subject" name="subject" /> </p> <p> <input type="radio" name="maru" value="TRUE" checked="checked" />O <input type="radio" name="maru" value="FALSE" />X </p> <input type="button" id="submit" value="Go" /> </body> </html>
# serverless.yml .... functions: lambda_handler: handler: handler.lambda_handler events: - http: path: / method: POST .... Action: - s3:GetObject - s3:PutObject
今度は入力欄があります。
適当に入力してPOST
します。
エラーです。
Oh, Why ?
index.html
を確認すると contentType: "application/json"
と記載されています。
Content-Typeは text/plain
、multipart/form-data
、application/x-www-form-urlencoded
じゃないぜ。
ってことは、シンプルなリクエストじゃないから、Access-Control-Allow-Origin
, Access-Control-Allow-Headers
ヘッダーが必要だぜ。
下記のようにhandler.py
をAccess-Control-Allow-Headers
ヘッダーを追記して更新します。 *2
import json def lambda_handler(event, context): print(event['httpMethod']) return { 'statusCode': 200, 'headers': { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", }, 'body': json.dumps(event['body']) }
そしたらデプロイしてクリックします。
Oh, またダメだよ!なぜだよ!
そうでした、ブラウザがシンプルではない HTTP リクエストを受信した場合は、ブラウザに実際のリクエストを送信する前に、サーバーに preflightリクエスト を送るのでした。。
CORS をサポートするために、REST API リソースは、OPTIONSメソッドで preflightリクエスト に応答できる必要がありました。
つまり、CORS を有効化ですね。
コンソールだとここですが、
serverless.yml
に cors: true
を追記してデプロイします。
.... functions: lambda_handler: handler: handler.lambda_handler events: - http: path: / method: POST cors: true ....
有効化されますね。
試します。
OKOK〜成功フォ〜ゥ。
greedyパス変数とANY使ってみる
OK, そしたら今度はgreedyパス変数とANY使ってみるぜ。
serverless.yml
と index.html
を下記のように更新してデプロイするぜ。
.... functions: lambda_handler: handler: handler.lambda_handler events: - http: path: /{proxy+} method: ANY ....
.... var URL = 'APIGATEWAY_ENDPOINT/api'; ....
コンソールだとこんな感じです。
POSTしてみます。
Oh, CORS有効化してないのにできたぜ。
CORS をサポートするために、REST API リソースは、フェッチ標準で必要な次のレスポンスヘッダーを使用して、少なくとも OPTIONS プリフライトリクエストに応答できる OPTIONS メソッドを実装する必要があります。
ANYメソッドは、元々OPTIONSにも対応できちゃうので、CORSの有効化をする必要がないのですね。
これは楽 & 便利だぜ。
説明しよう。
プロキシ統合とANYメソッドとgreedyパス変数について
よく一緒に使用されますが、greedy パス変数 `{proxy+} `、ANY メソッド、(HTTP/Lambda)プロキシ統合タイプはそれぞれ独立した機能です。
- プロキシ統合とは?
Lambda プロキシ統合では、クライアントが API リクエストを送信すると、API Gateway は、統合された Lambda 関数に raw リクエストをそのまま渡します。
- ANYメソッドとは?
汎用的な HTTP メソッドです。DELETE、GET、HEAD、OPTIONS、PATCH、POST および PUT のサポートされるすべての HTTP メソッドに対応します。
- greedyパス変数とは?
{proxy+}
の代わりに特定の URL パスを指定し、要求されるヘッダー、クエリ文字列パラメータ、または適切なペイロードを含めます
これで整理ができて、対応方法も確認できました。
そしたら、環境は消しましょう。
sls remove -v
最後に
Lambda プロキシ統合のCORS対応をやってみながらまとめてみました。
途中から何だか楽しくなってきて、語尾がおかしくなってしまいました。
大変失礼致しました。
以上です。
どなたかのお役に立てば幸いです。
参考:
- API Gateway REST API リソースの CORS を有効にする
- CORS(Cross-Origin Resource Sharing)について整理してみた
- Serverless Framework / API Gateway
- 初めてのサーバーレスアプリケーション開発 ~Serverless Framework を使ってAWSリソースをデプロイする~
- ServerlessFrameworkでS3の静的サイトのホスティングをする